package br.edu.ufcg.unknown.lexico;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.LinkedList;

import br.edu.ufcg.unknown.exception.EOFUnknownException;
import br.edu.ufcg.unknown.exception.UnknownException;

/**
 * Responsável pela leitura de caracteres de um código-fonte (arquivo ou
 * string).
 * 
 * @author Fábio Jorge
 * @author Jaindson Valentim
 * @author Moisés Rodrigues
 */
public class FluxoDeCaractere {

	public static char ESPACO_EM_BRANCO = ' ';
	public static char RETORNO_DE_CARRO = '\r';
	public static char TABULACAO = '\t';
	public static char QUEBRA_DE_LINHA = '\n';
	public static char PREENCHEDOR_DE_FOLHA = '\f';

	/**
	 * Quantidade máxima de Whitespaces em sequencia suportado.
	 */
	public static int QUANT_MAXIMA_DE_WHITESPACE_EM_SEQUENCIA = 100;

	@SuppressWarnings("unused")
	private File codigoFonte;

	/**
	 * Buffer de leitura dos caracteres.
	 */
	private BufferedReader br;

	private LinkedList<Character> buffer = new LinkedList<Character>();

	/**
	 * Quantidade de caracteres da última linha lida. Importante para que seja
	 * possível atualizar o valor da <i>linha</i> e <i>coluna</i> quando os
	 * caracteres são devolvidos ao fluxo.
	 */
	private LinkedList<Integer> quantCaracteresUltimaLinha = new LinkedList<Integer>();

	/**
	 * Armazena o valor da linha onde o último caractere foi lido.
	 * Quebra-de-linha é considerado como parte da linha seguinte.
	 */
	private int linha = 1;

	/**
	 * Armazena o valor da coluna onde o último caractere foi lido.
	 */
	private int coluna = 0;

	@SuppressWarnings("unused")
	private FluxoDeCaractere() {

	}

	/**
	 * Construtor utilizado passando todo o código-fonte em formato de string.
	 * 
	 * @param codigoFonte
	 *            código-fonte em formato de string.
	 */
	public FluxoDeCaractere(String codigoFonte) {
		this.codigoFonte = null;
		this.br = new BufferedReader(new StringReader(codigoFonte));
	}

	/**
	 * Construtor utilizado passando o código-fonte num arquivo.
	 * 
	 * @param codigoFonte
	 *            Arquivo onde o código-fonte está armazenado.
	 * @throws UnknownException
	 *             Caso o arquivo não seja encontrado.
	 */
	public FluxoDeCaractere(File codigoFonte) throws UnknownException {
		this.codigoFonte = codigoFonte;
		try {
			this.br = new BufferedReader(new FileReader(codigoFonte));
		} catch (FileNotFoundException e) {
			throw new UnknownException("Erro! Arquivo de código-fonte não encontrado: " + codigoFonte.getAbsolutePath(), e);
		}
	}

	/**
	 * @return True se tiver próximo caractere.
	 */
	public boolean temProxCaractere() {

		if (!this.buffer.isEmpty()) {
			return true;
		}

		try {
			this.br.mark(1);
			int read = this.br.read();
			this.br.reset();
			return read != -1 ? true : false;
		} catch (IOException e) {
			e.printStackTrace();
		}

		return false;
	}

	/**
	 * Le um caracter do fluxo de caracteres. Se nao houver caracter algum no fluxo
	 * um caracter null sera retornado.
	 * @return o caracter lido ou null se nao houver caracter no fluxo
	 * @throws IOException
	 */
	protected Character read() throws IOException {
		Character caractere = null;

		if (!this.buffer.isEmpty()) {
			caractere = this.buffer.removeFirst();
		} else {

			int read = this.br.read();
			if (read == -1) {
				return null;
			}
			caractere = (char) read;
		}

		if ('\n' == caractere) {
			this.quantCaracteresUltimaLinha.add(this.coluna);
			this.linha++;
			this.coluna = 0;
		} else {
			this.coluna++;
		}

		return caractere;
	}

	/**
	 * Retorna o próximo caractere válido para a Linguagem. Note que se um
	 * caractere inválido for lido ele é devolvido ao fluxo e uma exceção é
	 * lançada.
	 * 
	 * @return Próximo caractere.
	 */
	public Character getProxCaractere() {
		Character caractere = null;

		try {
			caractere = read();

			if (caractere == null) {
				throw new EOFUnknownException();
			}

		} catch (IOException e) {
			e.printStackTrace();
		}

		return caractere;
	}

	/**
	 * @return True se tiver próximo caractere que não seja whitespace.
	 */
	public boolean temProxCaractereIgWS() {

		if (!this.buffer.isEmpty()) {
			for (Character caractere : this.buffer) {
				if (!Character.isWhitespace(caractere)) {
					return true;
				}
			}

		}

		int read = 0;

		try {
			this.br.mark(QUANT_MAXIMA_DE_WHITESPACE_EM_SEQUENCIA);
			do {
				read = this.br.read();
			} while (read != -1 && Character.isWhitespace(read));

			this.br.reset();
		} catch (IOException e) {
			e.printStackTrace();
		}
		return read != -1 ? true : false;
	}

	/**
	 * @return Próximo caractere que não seja whitespace.
	 */
	public Character getProxCaractereIgWS() {
		Character caractere = null;

		while (Character.isWhitespace((caractere = getProxCaractere()))) {
		}

		return caractere;
	}

	/**
	 * Utilizado para devolver um Caractere lido do fluxo. Normalmente ocorre
	 * quando se está construindo, por exemplo, um <i>identificador</i> e um
	 * caractere a mais foi lido. Observe que não fazemos verificação pra saber
	 * se o caractere sendo devolvido foi realmente o último lido.
	 * 
	 * @param caractere
	 *            Caractere a ser devolvido ao fluxo.
	 */
	protected boolean devolver(Character caractere) {
		this.buffer.addFirst(caractere);
		if (caractere == '\n') {
			this.linha--;
			this.coluna = this.quantCaracteresUltimaLinha.removeLast();
		} else {
			this.coluna--;
		}

		return true;
	}

	/**
	 * A Linha onde o último caractere foi lido. Note que este método só garante
	 * corretude da informação caso sempre que for devolvido um caractere ao
	 * fluxo, o caractere '\n' também seja devolvido.
	 * 
	 * @return A Linha de onde o fluxo está sendo lido.
	 */
	public int getLinha() {
		return linha;
	}

	/**
	 * @return Valor da Linha do próximo caractere a ser lido. -1 Caso não haja
	 *         próximo caractere no fluxo.
	 */
	public int getLinhaProxCaractereIgWS() {
		int linha = -1;

		Character caractere = null;
		LinkedList<Character> caracteresLidos = new LinkedList<Character>();
		while (temProxCaractere()) {
			caractere = getProxCaractere();
			linha = getLinha();
			caracteresLidos.addLast(caractere);
			if (!Character.isWhitespace(caractere)) {
				break;
			}
		}

		while (caracteresLidos.size() > 0) {
			devolver(caracteresLidos.removeLast());
		}

		return linha;
	}

	/**
	 * @return A Coluna da Linha onde o último caractere foi lido.
	 */
	public int getColuna() {
		return coluna;
	}

	/**
	 * @return Valor da Coluna do próximo caractere a ser lido. -1 Caso não haja
	 *         próximo caractere no fluxo.
	 */
	public int getColunaProxCaractereIgWS() {
		int coluna = -1;

		Character caractere = null;
		LinkedList<Character> caracteresLidos = new LinkedList<Character>();
		while (temProxCaractere()) {
			caractere = getProxCaractere();
			coluna = getColuna();
			caracteresLidos.add(caractere);
			if (!Character.isWhitespace(caractere)) {
				break;
			}
		}

		while (caracteresLidos.size() > 0) {
			devolver(caracteresLidos.removeLast());
		}

		return coluna;
	}

}
